<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title>NumPy Vectors & Matrices — Pyodide Lab</title>

    <!-- Tailwind (optional, used for the utility classes in your component) -->
    <script src="https://cdn.tailwindcss.com"></script>
    <style>
      /* Small safety styles so it looks okay even if some Tailwind classes are missing */
      body { background: #f8fafc; }
      .container { max-width: 1100px; margin: 0 auto; }
    </style>

    <!-- React 18 + ReactDOM 18 (UMD builds) -->
    <script crossorigin src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
    <script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
    
    <!-- Babel Standalone to compile JSX in-browser -->
    <script src="https://unpkg.com/@babel/standalone@7/babel.min.js"></script>
  </head>
  <body>
    <div id="app" class="container"></div>

    <!-- Your app code: paste your component here. Type text/babel so JSX works. -->
    <script type="text/babel">
      const { useEffect, useMemo, useState } = React;

      // NumPy Basics with Pyodide — Interactive Canvas (Patched)
      function NumpyVectorsMatricesPyodide_Patched() {
        const [pyodide, setPyodide] = useState(null);
        const [status, setStatus] = useState("Loading Pyodide…");
        const [ready, setReady] = useState(false);
        const [running, setRunning] = useState(false);

        // --- Load Pyodide + NumPy ---
        useEffect(() => {
          let cancelled = false;
          async function boot() {
            try {
              setStatus("Loading Python runtime…");
              // Inject the Pyodide script if not present
              await new Promise((resolve, reject) => {
                if (window.loadPyodide) return resolve();
                const script = document.createElement("script");
                script.src = "https://cdn.jsdelivr.net/pyodide/v0.25.1/full/pyodide.js";
                script.onload = resolve;
                script.onerror = reject;
                document.body.appendChild(script);
              });
              setStatus("Starting Pyodide…");
              const py = await window.loadPyodide({ indexURL: "https://cdn.jsdelivr.net/pyodide/v0.25.1/full/" });
              setStatus("Installing NumPy…");
              await py.loadPackage("numpy");
              // Warmup to verify stdout capture
              await py.runPythonAsync("import numpy as np\nprint('pyodide warmup ok')\n'ready'");
              if (!cancelled) {
                setPyodide(py);
                setReady(true);
                setStatus("Ready");
              }
            } catch (err) {
              console.error(err);
              setStatus("Failed to load Pyodide. Please reload the page.");
            }
          }
          boot();
          return () => { cancelled = true; };
        }, []);

        // --- Lesson text (quick reference) ---
        const lesson = (
          <div className="prose prose-slate max-w-none">
            <h1>NumPy: Basic Vector & Matrix Operations</h1>
            <p>
              A <strong>vector</strong> is a 1D array (shape like <code>(n,)</code>), and a <strong>matrix</strong> is a 2D array (shape like <code>(m, n)</code>).
              In NumPy, both are represented by <code>numpy.ndarray</code>.
            </p>
            <ul>
              <li>
                Create arrays: <code>a = np.array([1, 2, 3])</code>,
                <code>A = np.array([[1,2],[3,4]])</code>
              </li>
              <li>
                Shapes: <code>a.shape</code>, <code>A.shape</code>
              </li>
              <li>
                Elementwise ops: <code>a + b</code>, <code>2*a - b</code>, <code>a * b</code>
              </li>
              <li>
                Dot product: <code>np.dot(a, b)</code>
              </li>
              <li>
                Matrix multiply: <code>A @ B</code> or <code>np.matmul(A, B)</code>
              </li>
              <li>
                Transpose: <code>A.T</code>
              </li>
              <li>
                Indexing/slicing: <code>A[0, 1]</code>, <code>A[:, 0]</code>, <code>A[1:3, :]</code>
              </li>
              <li>
                Broadcasting: <code>A + v</code> (if shapes are compatible)
              </li>
              <li>
                Norm: <code>np.linalg.norm(a)</code>
              </li>
            </ul>
          </div>
        );

        // --- Exercises ---
        const exercises = useMemo(() => [
          {
            id: "ex1",
            title: "Vectors: create & elementwise operations",
            prompt:
              "Create 1D vectors a and b of length 3. Compute c = a + b and d = 2*a - b. Print c and d, and confirm shapes are (3,).",
            starter: `import numpy as np\n\n# Create vectors a and b (length 3)\na = np.array([1, 2, 3])  # you may change values\nb = np.array([4, 5, 6])\n\n# Compute elementwise results\nc = a + b\nd = 2*a - b\n\nprint('a shape:', a.shape)\nprint('b shape:', b.shape)\nprint('c:', c)\nprint('d:', d)\n`,
            validator: `def validate(ns):\n    import numpy as np\n    a, b = ns.get('a'), ns.get('b')\n    c, d = ns.get('c'), ns.get('d')\n    assert isinstance(a, np.ndarray) and isinstance(b, np.ndarray), 'a and b must be numpy arrays'\n    assert a.shape == (3,) and b.shape == (3,), 'a and b must have shape (3,)'\n    assert isinstance(c, np.ndarray) and isinstance(d, np.ndarray), 'compute c and d as arrays'\n    assert np.allclose(c, a + b), 'c must equal a + b'\n    assert np.allclose(d, 2*a - b), 'd must equal 2*a - b'\n    print('✅ Passed: shapes and elementwise ops are correct!')\n    return True\n`,
          },
          {
            id: "ex2",
            title: "Dot product & Euclidean norm",
            prompt:
              "Given vectors a and b (same length), compute dot = a·b and the Euclidean norm of a. Print both.",
            starter: `import numpy as np\n\n# Define vectors a and b\na = np.array([1., 2., 3.])\nb = np.array([0.5, -1., 4.])\n\n# Compute the dot product and the norm of a\ndot = np.dot(a, b)\nna = np.linalg.norm(a)\n\nprint('dot(a,b) =', dot)\nprint('||a|| =', na)\n`,
            validator: `def validate(ns):\n    import numpy as np\n    a, b = ns.get('a'), ns.get('b')\n    dot, na = ns.get('dot'), ns.get('na')\n    assert a is not None and b is not None, 'define a and b'\n    assert np.isscalar(dot), 'dot should be a scalar'\n    assert np.isscalar(na), 'na should be a scalar'\n    assert np.isclose(dot, float(np.dot(a, b))), 'dot product incorrect'\n    assert np.isclose(na, float(np.linalg.norm(a))), 'norm incorrect'\n    print('✅ Passed: dot product and norm look good!')\n    return True\n`,
          },
          {
            id: "ex3",
            title: "Matrix multiply & transpose",
            prompt:
              "Create A (2×3) and B (3×2). Compute C = A @ B, its shape, and C[0,1]. Also compute A.T (transpose).",
            starter: `import numpy as np\n\n# Define matrices with compatible shapes\nA = np.array([[1, 2, 3],\n              [4, 5, 6]])   # 2x3\nB = np.array([[7,  8],\n              [9, 10],\n              [11,12]])     # 3x2\n\nC = A @ B\nprint('C shape:', C.shape)\nprint('C[0,1]:', C[0,1])\nprint('A.T:\\n', A.T)\n`,
            validator: `def validate(ns):\n    import numpy as np\n    A, B, C = ns.get('A'), ns.get('B'), ns.get('C')\n    assert A.shape == (2,3) and B.shape == (3,2), 'A must be 2x3 and B must be 3x2'\n    assert C.shape == (2,2), 'C should be 2x2'\n    assert np.allclose(C, A @ B), 'C must equal A @ B'\n    assert np.allclose(ns.get('A').T, ns.get('A').transpose()), 'transpose should match'\n    print('✅ Passed: matmul, shape, index, and transpose are correct!')\n    return True\n`,
          },
          {
            id: "ex4",
            title: "Broadcasting a row vector",
            prompt:
              "Create M (3×3) and row vector v (shape (3,)). Compute S = M + v (adds v to each row). Print S.",
            starter: `import numpy as np\n\nM = np.array([[1,2,3],\n              [4,5,6],\n              [7,8,9]])\nv = np.array([10,20,30])\n\nS = M + v\nprint(S)\n`,
            validator: `def validate(ns):\n    import numpy as np\n    M, v, S = ns.get('M'), ns.get('v'), ns.get('S')\n    assert M.shape == (3,3), 'M must be 3x3'\n    assert v.shape == (3,), 'v must be shape (3,)'\n    assert np.allclose(S, M + v), 'S must equal M + v with broadcasting'\n    print('✅ Passed: broadcasting works!')\n    return True\n`,
          },
          {
            id: "ex5",
            title: "Indexing and slicing",
            prompt:
              "From a 4×4 matrix X, extract the 2×2 top-left block (call it TL) and the last column (call it last_col). Print both.",
            starter: `import numpy as np\n\nX = np.arange(1, 17).reshape(4,4)\n#  X = [[ 1,  2,  3,  4],\n#       [ 5,  6,  7,  8],\n#       [ 9, 10, 11, 12],\n#       [13, 14, 15, 16]]\n\nTL = X[0:2, 0:2]\nlast_col = X[:, 3]\n\nprint('TL:\\n', TL)\nprint('last_col:', last_col)\n`,
            validator: `def validate(ns):\n    import numpy as np\n    X = ns.get('X')\n    TL = ns.get('TL')\n    last_col = ns.get('last_col')\n    assert X.shape == (4,4), 'X must be 4x4'\n    assert TL.shape == (2,2), 'TL must be 2x2'\n    assert last_col.shape == (4,), 'last_col must be a 1D length-4 vector'\n    assert np.allclose(TL, X[:2, :2]), 'TL should be top-left 2x2'\n    assert np.allclose(last_col, X[:, -1]), 'last_col should be last column'\n    print('✅ Passed: indexing and slicing look correct!')\n    return True\n`,
          },
        ], []);

        // --- Helpers to run Python ---
        async function runUserCode(code) {
          if (!pyodide) return { user: "", checks: "Pyodide not ready." };
          const py = pyodide;
          const pySrc = `\nimport sys, io, contextlib, numpy as np\n_user_out = ''\nns = {'np': np}\nwith io.StringIO() as buf, contextlib.redirect_stdout(buf):\n    try:\n        exec(${JSON.stringify(code)}, ns)\n    except Exception as e:\n        print('❌ Error while running your code:', e)\n    _user_out = buf.getvalue()\n_user_out\n`;
          try {
            const out = await py.runPythonAsync(pySrc);
            const outStr = out && out.toString ? out.toString() : String(out ?? '');
            return { user: outStr, checks: "" };
          } catch (e) {
            return { user: "", checks: String(e) };
          }
        }

        async function runAndCheck(code, validatorSrc) {
          if (!pyodide) return { user: "", checks: "Pyodide not ready." };
          const py = pyodide;
          const pySrc = `\nimport sys, io, contextlib, numpy as np\n_user_out = ''\n_check_out = ''\n_ok = False\nns = {'np': np}\n# Run user code and capture prints\nwith io.StringIO() as buf, contextlib.redirect_stdout(buf):\n    try:\n        exec(${JSON.stringify(code)}, ns)\n    except Exception as e:\n        print('❌ Error while running your code:', e)\n    _user_out = buf.getvalue()\n\n# Define validator\n${validatorSrc}\n\n# Run validator and capture prints\nwith io.StringIO() as buf, contextlib.redirect_stdout(buf):\n    try:\n        _ok = bool(validate(ns))\n    except AssertionError as e:\n        print('❌', e)\n        _ok = False\n    except Exception as e:\n        print('❌ Validator error:', e)\n        _ok = False\n    _check_out = buf.getvalue()\n\n{'ok': _ok, 'user': _user_out, 'checks': _check_out}\n`;
          try {
            const res = await py.runPythonAsync(pySrc);
            let jsRes;
            try {
              jsRes = res.toJs ? res.toJs({ dict_converter: Object.fromEntries }) : res;
            } catch {
              jsRes = res;
            }
            if (jsRes instanceof Map) jsRes = Object.fromEntries(jsRes);
            if (typeof jsRes.user !== 'string') jsRes.user = String(jsRes.user ?? '');
            if (typeof jsRes.checks !== 'string') jsRes.checks = String(jsRes.checks ?? '');
            return jsRes;
          } catch (e) {
            return { ok: false, user: "", checks: String(e) };
          }
        }

        // --- Exercise Card Component ---
        function ExerciseCard({ idx, ex }) {
          const [code, setCode] = useState(ex.starter);
          const [output, setOutput] = useState("");
          const [checks, setChecks] = useState("");
          const [ok, setOk] = useState(null);

          const onRun = async () => {
            setChecks("");
            const res = await runUserCode(code);
            setOutput(res.user ?? String(res));
          };

          const onCheck = async () => {
            const res = await runAndCheck(code, ex.validator);
            setOutput(res.user ?? "");
            setChecks(res.checks ?? "");
            setOk(Boolean(res.ok));
          };

          const onReset = () => {
            setCode(ex.starter);
            setOutput("");
            setChecks("");
            setOk(null);
          };

          return (
            <div className="rounded-2xl border bg-white p-4 shadow-sm">
              <div className="flex items-start justify-between gap-4">
                <div>
                  <h3 className="text-lg font-semibold">{idx}. {ex.title}</h3>
                  <p className="mt-1 text-sm text-slate-600">{ex.prompt}</p>
                </div>
                <div className="flex items-center gap-2">
                  {ok === true && <span title="Passed" className="text-2xl">✅</span>}
                  {ok === false && <span title="Needs work" className="text-2xl">❌</span>}
                </div>
              </div>

              <div className="mt-3 grid gap-3">
                <textarea
                  value={code}
                  onChange={(e) => setCode(e.target.value)}
                  className="min-h-[160px] w-full resize-y rounded-xl border p-3 font-mono text-sm"
                  spellCheck={false}
                />

                <div className="flex flex-wrap items-center gap-2">
                  <button
                    type="button"
                    onClick={onRun}
                    disabled={!ready}
                    className="rounded-xl bg-black px-4 py-2 text-white disabled:opacity-50"
                  >
                    ▶ Run
                  </button>
                  <button
                    type="button"
                    onClick={onCheck}
                    disabled={!ready}
                    className="rounded-xl border px-4 py-2 disabled:opacity-50"
                  >
                    ✓ Check
                  </button>
                  <button
                    type="button"
                    onClick={onReset}
                    className="rounded-xl border px-4 py-2"
                  >
                    ↺ Reset
                  </button>
                </div>

                <div className="grid gap-2">
                  <label className="text-xs font-semibold uppercase tracking-wide text-slate-500">Output</label>
                  <pre className="min-h-[64px] whitespace-pre-wrap rounded-xl bg-slate-50 p-3 text-sm">{output || "(no output)"}</pre>
                </div>

                <div className="grid gap-2">
                  <label className="text-xs font-semibold uppercase tracking-wide text-slate-500">Checks</label>
                  <pre className="min-h-[40px] whitespace-pre-wrap rounded-xl bg-slate-50 p-3 text-sm">{checks || "(run ✓ Check to see feedback)"}</pre>
                </div>
              </div>
            </div>
          );
        }

        // --- Playground ---
        const [playCode, setPlayCode] = useState(`import numpy as np\n\n# Try anything here. Some quick demos:\na = np.array([1, 2, 3])\nb = np.array([4, 5, 6])\nprint('a + b =', a + b)\n\nA = np.array([[1,2],[3,4]])\nB = np.array([[5,6],[7,8]])\nprint('A @ B =\\n', A @ B)\n`);
        const [playOut, setPlayOut] = useState("");

        const onRunPlay = async () => {
          const res = await runUserCode(playCode);
          setPlayOut(res.user ?? String(res));
        };

        return (
          <div className="mx-auto max-w-5xl p-6">
            <div className="mb-6 rounded-2xl border bg-white p-5 shadow-sm">
              <div className="flex items-center justify-between">
                <h2 className="text-2xl font-bold">NumPy Basics — Interactive Lab</h2>
                <span className="text-sm text-slate-600">{status}</span>
              </div>
              <p className="mt-2 text-slate-700">
                Learn core vector & matrix operations in NumPy, then practice with short exercises. Use the
                Playground at the end to experiment. If something fails, read the error in the Output pane and
                try again — that’s normal while learning!
              </p>
            </div>

            <div className="mb-6 rounded-2xl border bg-white p-5 shadow-sm">{lesson}</div>

            <div className="grid gap-5">
              {exercises.map((ex, i) => (
                <ExerciseCard key={ex.id} idx={i + 1} ex={ex} />
              ))}
            </div>

            <div className="mt-8 rounded-2xl border bg-white p-5 shadow-sm">
              <h3 className="text-lg font-semibold">Playground</h3>
              <p className="mt-1 text-sm text-slate-600">
                A free space to run any NumPy code. NumPy is preloaded as <code>np</code>.
              </p>
              <div className="mt-3 grid gap-3">
                <textarea
                  value={playCode}
                  onChange={(e) => setPlayCode(e.target.value)}
                  className="min-h-[160px] w-full resize-y rounded-xl border p-3 font-mono text-sm"
                  spellCheck={false}
                />
                <div className="flex items-center gap-2">
                  <button
                    type="button"
                    onClick={onRunPlay}
                    disabled={!ready}
                    className="rounded-2xl bg-black px-4 py-2 text-white disabled:opacity-50"
                  >
                    ▶ Run
                  </button>
                </div>
                <div className="grid gap-2">
                  <label className="text-xs font-semibold uppercase tracking-wide text-slate-500">Output</label>
                  <pre className="min-h-[64px] whitespace-pre-wrap rounded-xl bg-slate-50 p-3 text-sm">{playOut || "(no output)"}</pre>
                </div>
              </div>
            </div>

            <footer className="mt-10 text-center text-xs text-slate-500">
              Tip: Use <code>a.shape</code> and <code>A.shape</code> often to sanity-check your math.
            </footer>
          </div>
        );
      }

      // Mount the app
      const root = ReactDOM.createRoot(document.getElementById('app'));
      root.render(React.createElement(NumpyVectorsMatricesPyodide_Patched));
    </script>
  </body>
</html>
